/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements. See the NOTICE file distributed with this
 * work for additional information regarding copyright ownership. The ASF
 * licenses this file to You under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 * http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 * 
 * NOTE: This code has been adapted from code in the
 * org.apache.tools.ant.types.resources.Files class.
 * 
 * Changes copyright 2009 by Michael Brennan.
 */

package eclant.ant.types;

import java.io.File;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Project;
import org.apache.tools.ant.types.PatternSet;
import org.apache.tools.ant.types.Reference;
import org.apache.tools.ant.types.selectors.AbstractSelectorContainer;
import org.apache.tools.ant.types.selectors.FileSelector;

public abstract class AbstractPatternContainer extends AbstractSelectorContainer implements
		Cloneable
{
	private PatternSet defaultPatterns = new PatternSet();
	private List<PatternSet> additionalPatterns = new ArrayList<PatternSet>();

	private boolean useDefaultExcludes = true;
	private boolean caseSensitive = true;
	private boolean followSymlinks = true;

	/**
	 * Construct a new <code>Files</code> collection.
	 */
	public AbstractPatternContainer()
	{
		super();
	}

	/**
	 * Construct a new <code>AbstractPatternContainer</code> collection,
	 * shallowly cloned from the specified <code>AbstractPatternContainer</code>
	 * .
	 * 
	 * @param f
	 *            the <code>AbstractPatternContainer</code> to use as a
	 *            template.
	 */
	protected AbstractPatternContainer(AbstractPatternContainer f)
	{
		this.defaultPatterns = f.defaultPatterns;
		this.additionalPatterns = f.additionalPatterns;
		this.useDefaultExcludes = f.useDefaultExcludes;
		this.caseSensitive = f.caseSensitive;
		this.followSymlinks = f.followSymlinks;
		setProject(f.getProject());
	}

	/**
	 * Make this instance in effect a reference to another instance.
	 * 
	 * <p>
	 * You must not set another attribute or nest elements inside this element
	 * if you make it a reference.
	 * </p>
	 * 
	 * @param r
	 *            the <code>Reference</code> to use.
	 * @throws BuildException
	 *             if there is a problem.
	 */
	@Override
	public void setRefid(Reference r) throws BuildException
	{
		if (hasPatterns(defaultPatterns))
		{
			throw tooManyAttributes();
		}
		if (!additionalPatterns.isEmpty())
		{
			throw noChildrenAllowed();
		}
		if (hasSelectors())
		{
			throw noChildrenAllowed();
		}
		super.setRefid(r);
	}

	/**
	 * Create a nested patternset.
	 * 
	 * @return <code>PatternSet</code>.
	 */
	public synchronized PatternSet createPatternSet()
	{
		if (isReference())
		{
			throw noChildrenAllowed();
		}
		PatternSet patterns = new PatternSet();
		additionalPatterns.add(patterns);
		return patterns;
	}

	/**
	 * Add a name entry to the include list.
	 * 
	 * @return <code>PatternSet.NameEntry</code>.
	 */
	public synchronized PatternSet.NameEntry createInclude()
	{
		if (isReference())
		{
			throw noChildrenAllowed();
		}
		return defaultPatterns.createInclude();
	}

	/**
	 * Add a name entry to the include files list.
	 * 
	 * @return <code>PatternSet.NameEntry</code>.
	 */
	public synchronized PatternSet.NameEntry createIncludesFile()
	{
		if (isReference())
		{
			throw noChildrenAllowed();
		}
		return defaultPatterns.createIncludesFile();
	}

	/**
	 * Add a name entry to the exclude list.
	 * 
	 * @return <code>PatternSet.NameEntry</code>.
	 */
	public synchronized PatternSet.NameEntry createExclude()
	{
		if (isReference())
		{
			throw noChildrenAllowed();
		}
		return defaultPatterns.createExclude();
	}

	/**
	 * Add a name entry to the excludes files list.
	 * 
	 * @return <code>PatternSet.NameEntry</code>.
	 */
	public synchronized PatternSet.NameEntry createExcludesFile()
	{
		if (isReference())
		{
			throw noChildrenAllowed();
		}
		return defaultPatterns.createExcludesFile();
	}

	/**
	 * Append <code>includes</code> to the current list of include patterns.
	 * 
	 * <p>
	 * Patterns may be separated by a comma or a space.
	 * </p>
	 * 
	 * @param includes
	 *            the <code>String</code> containing the include patterns.
	 */
	public synchronized void setIncludes(String includes)
	{
		checkAttributesAllowed();
		defaultPatterns.setIncludes(includes);
	}

	/**
	 * Append <code>includes</code> to the current list of include patterns.
	 * 
	 * @param includes
	 *            array containing the include patterns.
	 */
	public synchronized void appendIncludes(String[] includes)
	{
		checkAttributesAllowed();
		if (includes != null)
		{
			for (int i = 0; i < includes.length; i++)
			{
				defaultPatterns.createInclude().setName(includes[i]);
			}
		}
	}

	/**
	 * Append <code>excludes</code> to the current list of exclude patterns.
	 * 
	 * <p>
	 * Patterns may be separated by a comma or a space.
	 * </p>
	 * 
	 * @param excludes
	 *            the <code>String</code> containing the exclude patterns.
	 */
	public synchronized void setExcludes(String excludes)
	{
		checkAttributesAllowed();
		defaultPatterns.setExcludes(excludes);
	}

	/**
	 * Append <code>excludes</code> to the current list of include patterns.
	 * 
	 * @param excludes
	 *            array containing the exclude patterns.
	 */
	public synchronized void appendExcludes(String[] excludes)
	{
		checkAttributesAllowed();
		if (excludes != null)
		{
			for (int i = 0; i < excludes.length; i++)
			{
				defaultPatterns.createExclude().setName(excludes[i]);
			}
		}
	}

	/**
	 * Set the <code>File</code> containing the includes patterns.
	 * 
	 * @param incl
	 *            <code>File</code> instance.
	 * @throws BuildException
	 *             if there is a problem.
	 */
	public synchronized void setIncludesfile(File incl) throws BuildException
	{
		checkAttributesAllowed();
		defaultPatterns.setIncludesfile(incl);
	}

	/**
	 * Set the <code>File</code> containing the excludes patterns.
	 * 
	 * @param excl
	 *            <code>File</code> instance.
	 * @throws BuildException
	 *             if there is a problem.
	 */
	public synchronized void setExcludesfile(File excl) throws BuildException
	{
		checkAttributesAllowed();
		defaultPatterns.setExcludesfile(excl);
	}

	/**
	 * Set whether default exclusions should be used or not.
	 * 
	 * @param useDefaultExcludes
	 *            <code>boolean</code>.
	 */
	public synchronized void setDefaultexcludes(boolean useDefaultExcludes)
	{
		checkAttributesAllowed();
		this.useDefaultExcludes = useDefaultExcludes;
	}

	/**
	 * Get whether default exclusions should be used or not.
	 * 
	 * @return the defaultexclusions value.
	 */
	public synchronized boolean getDefaultexcludes()
	{
		return (isReference()) ? getRef().getDefaultexcludes() : useDefaultExcludes;
	}

	/**
	 * Set case-sensitivity of the Files collection.
	 * 
	 * @param caseSensitive
	 *            <code>boolean</code>.
	 */
	public synchronized void setCaseSensitive(boolean caseSensitive)
	{
		checkAttributesAllowed();
		this.caseSensitive = caseSensitive;
	}

	/**
	 * Find out if this Files collection is case-sensitive.
	 * 
	 * @return <code>boolean</code> indicating whether the Files collection is
	 *         case-sensitive.
	 */
	public synchronized boolean isCaseSensitive()
	{
		return (isReference()) ? getRef().isCaseSensitive() : caseSensitive;
	}

	/**
	 * Set whether or not symbolic links should be followed.
	 * 
	 * @param followSymlinks
	 *            whether or not symbolic links should be followed.
	 */
	public synchronized void setFollowSymlinks(boolean followSymlinks)
	{
		checkAttributesAllowed();
		this.followSymlinks = followSymlinks;
	}

	/**
	 * Find out whether symbolic links should be followed.
	 * 
	 * @return <code>boolean</code> indicating whether symbolic links should be
	 *         followed.
	 */
	public synchronized boolean isFollowSymlinks()
	{
		return (isReference()) ? getRef().isFollowSymlinks() : followSymlinks;
	}


	/**
	 * Find out whether this Files collection has patterns.
	 * 
	 * @return whether any patterns are in this container.
	 */
	public synchronized boolean hasPatterns()
	{
		if (isReference())
		{
			return getRef().hasPatterns();
		}
		if (hasPatterns(defaultPatterns))
		{
			return true;
		}
		for (PatternSet ps : additionalPatterns)
		{
			if (hasPatterns(ps))
				return true;
		}
		return false;
	}

	/**
	 * Add a new selector into this container.
	 * 
	 * @param selector
	 *            the new <code>FileSelector</code> to add.
	 */
	@Override
	public synchronized void appendSelector(FileSelector selector)
	{
		if (isReference())
		{
			throw noChildrenAllowed();
		}
		super.appendSelector(selector);
	}

	/**
	 * Format this Files collection as a String.
	 * 
	 * @return a descriptive <code>String</code>.
	 */
	@Override
	public String toString()
	{
		if (isReference())
		{
			return getRef().toString();
		}
		StringBuilder sb = new StringBuilder();
		sb.append(defaultPatterns.toString());
		for (PatternSet ps : additionalPatterns)
		{
			sb.append(", ").append(ps.toString());
		}
		sb.append(super.toString());
		return sb.toString();
	}

	/**
	 * Create a deep clone of this instance, except for the nested selectors
	 * (the list of selectors is a shallow clone of this instance's list).
	 * 
	 * @return a cloned Object.
	 */
	@Override
	public synchronized Object clone()
	{
		if (isReference())
		{
			return getRef().clone();
		}
		try
		{
			AbstractPatternContainer set = (AbstractPatternContainer) super.clone();
			set.defaultPatterns = (PatternSet) defaultPatterns.clone();
			set.additionalPatterns = new ArrayList<PatternSet>(additionalPatterns.size());
			for (Iterator<PatternSet> iter = additionalPatterns.iterator(); iter.hasNext();)
			{
				PatternSet ps = iter.next();
				set.additionalPatterns.add((PatternSet) ps.clone());
			}
			return set;
		}
		catch (CloneNotSupportedException e)
		{
			throw new BuildException(e);
		}
	}

	/**
	 * Make sure we have all necessary attributes set
	 */
	@Override
	public void validate() throws BuildException
	{
		super.validate();
	}

	/**
	 * Get the merged include patterns for this Files collection.
	 * 
	 * @param p
	 *            Project instance.
	 * @return the include patterns of the default pattern set and all nested
	 *         patternsets.
	 */
	protected String[] mergeIncludes(Project p)
	{
		return mergePatterns(p).getIncludePatterns(p);
	}

	/**
	 * Get the merged exclude patterns for this Files collection.
	 * 
	 * @param p
	 *            Project instance.
	 * @return the exclude patterns of the default pattern set and all nested
	 *         patternsets.
	 */
	protected String[] mergeExcludes(Project p)
	{
		return mergePatterns(p).getExcludePatterns(p);
	}

	/**
	 * Get the merged patterns for this Files collection.
	 * 
	 * @param p
	 *            Project instance.
	 * @return the default patternset merged with the additional sets in a new
	 *         PatternSet instance.
	 */
	protected synchronized PatternSet mergePatterns(Project p)
	{
		if (isReference())
		{
			return getRef().mergePatterns(p);
		}
		PatternSet ps = new PatternSet();
		ps.append(defaultPatterns, p);
		final int count = additionalPatterns.size();
		for (int i = 0; i < count; i++)
		{
			Object o = additionalPatterns.get(i);
			ps.append((PatternSet) o, p);
		}
		return ps;
	}

	/**
	 * Perform the check for circular references and return the referenced Files
	 * collection.
	 * 
	 * @return <code>FileCollection</code>.
	 */
	protected AbstractPatternContainer getRef()
	{
		return (AbstractPatternContainer) getCheckedRef();
	}

	protected synchronized void setupResourceSelector(ResourceSelector rs)
	{
		PatternSet ps = mergePatterns(getProject());
		rs.setIncludes(ps.getIncludePatterns(getProject()));
		rs.setExcludes(ps.getExcludePatterns(getProject()));
		rs.setSelectors(getSelectors(getProject()));
		if (useDefaultExcludes)
		{
			rs.addDefaultExcludes();
		}
		rs.setCaseSensitive(caseSensitive);
		// rs.setFollowSymlinks(followSymlinks);
	}

	@Override
	protected BuildException circularReference()
	{
		return new BuildException("This data type contains a circular reference.", getLocation());
	}

	@Override
	protected BuildException noChildrenAllowed()
	{
		return new BuildException("You must not specify nested elements when using refid",
				getLocation());
	}

	@Override
	protected BuildException tooManyAttributes()
	{
		return new BuildException("You must not specify more than one attribute when using refid",
				getLocation());
	}

	private boolean hasPatterns(PatternSet ps)
	{
		String[] includePatterns = ps.getIncludePatterns(getProject());
		String[] excludePatterns = ps.getExcludePatterns(getProject());
		return (includePatterns != null && includePatterns.length > 0)
				|| (excludePatterns != null && excludePatterns.length > 0);
	}

}